混沌工程与 Game Day:把 Runbook 拿去撞一次墙 原创
本文是 SRE 实战系列第 20 篇。第 18 篇结尾留了一句话:下一篇讲"故障演练与混沌工程——不是演练给别人看,而是发现 Runbook 里哪一步会卡住"。这篇把那句话兑现。
全文约 1.3 万字。从一次真实的 Game Day 开头——四条 Runbook 里只有一条走通,而走不通的三条,卡住的地方跟技术能力毫无关系。然后回答五个问题:混沌实验跟"故意搞破坏"差在哪、Chaos Mesh 到底怎么落地、爆炸半径怎么算、Game Day 一天怎么组织、演练发现的问题怎么变成可跟踪的改进而不是一场热闹。
这篇的立场:韧性不是设计出来的,是被验证出来的。而验证的唯一方式,是在你能承受的代价下,让它真的坏一次。
第 19 篇讲怎么让变更不引入故障;这篇讲怎么主动制造故障,去证明在故障面前,你和你的团队还能动。
一、四个 Runbook,一条走通
在一个电商平台中,核心链路四个服务:网关、订单、支付、库存。这四个服务各有十来份 Runbook,是过去半年按第 18 篇那套十四段模板补齐的,文档结构评审全部通过,命令也都"当场跑过一遍"。
演练的设定很简单:让订单服务的一部分 Pod 在预发环境里连不上数据库,看值班同学能不能按 Runbook 走过去。
四个人分别扮演 on-call,每个人处理一个服务,全程录屏,真实告警、真实通知、真实权限。
- 网关同学:17 分钟,全部走完。Runbook 里"切换上游权重"那条命令是现成的,唯一卡了一下是在确认"这个权重改的是灰度还是主链路",翻到第二段判断树就找到了
- 订单同学:41 分钟,中途两次找我求助。卡点不在技术:Runbook 写的是
kubectl exec进 Pod 看连接数,但演练用的账号没有 exec 权限。他花了 9 分钟确认自己是不是敲错命令 - 支付同学:28 分钟,走完了,但走出了一条 Runbook 里没有的路。Runbook 第 3 步写"重启 Pod",他判断重启会引发二次雪崩,自己加了限流。这个判断是对的,但Runbook 里没有任何一处告诉他"什么时候不该重启"
- 库存同学:35 分钟,最后失败了。Runbook 第 4 步是"联系 DBA 确认主从延迟",而演练在周末下午,DBA 不在线。Runbook 里没有兜底动作,也没有写"联系不上怎么办"
四个服务的工程能力没有差别,Runbook 的结构完整度也一样。走不通的三条,卡点分别是权限、判断、人——没有一个是"技术不会"。
这就是这一篇要讲的事。演练的价值不在于证明系统很健壮,也不在于证明文档写得好;演练的价值在于它会在一个便宜的下午,把这些平时永远看不见的裂缝一次性照出来。
如果这四条 Runbook 是在真实故障里第一次被使用,代价会是 40 分钟 × 影响面。而那天下午的总成本是:四个工程师、四小时、预发环境的一点资源。
二、先划清界线:混沌实验不是搞破坏
很多人对"混沌工程"的第一反应是"故意把生产搞坏?图什么"。所以先把三件事分开。
2.1 它和另外三件事不一样
| 做的事 | 目的 | 什么时候做 | 有没有假设 |
|---|---|---|---|
| 测试 | 验证已知路径正确 | 变更前后 | 有明确断言(输入 A 应输出 B) |
| 压测 | 找容量上限 | 大促前、扩容前 | 有明确指标(QPS → 延迟曲线) |
| 故障注入 / 混沌实验 | 验证"故障发生时系统与人的反应" | 平时 | 有假设,但断言是"稳态应该保持" |
| Game Day | 把上面几件事组织成一次全员参与的活动 | 每季度 | 多个假设,加上人的因素 |
关键区别在断言的形式。测试断言"结果等于期望值",压测断言"延迟不超阈值",而混沌实验断言的是稳态:
"在 X 故障持续 Y 分钟、影响 Z 比例实例的条件下,核心 SLI 不跌破 SLO,且告警在 T 秒内触发,且 Runbook 能让一个不认识这个服务的人在 N 分钟内恢复。"
这个断言有三段,很多人只做第一段。只验证"系统扛住了",就只做了一半的混沌工程——因为你验证的是系统,而 03:00 报警时要动的是人。
2.2 一个必须先想清楚的问题:为什么要主动制造故障
因为有两类事实,你永远无法通过观察得到:
第一类:系统在故障下的行为。 平时的监控只能看到"正常运行"这一种状态。系统在数据库连不上时是快速失败还是排队卡死?重试风暴会不会把下游压垮?降级开关真的生效吗,还是那条分支三年没走过、现在就 panic?这些问题,只有真的断一次才知道。
一个常见的现场:某服务加了熔断器,配置了"下游错误率超 50% 熔断 30 秒"。看起来很稳。演练时把下游打挂,熔断器确实触发了,但触发之后把请求全部打到备用的老接口,而那个老接口早在两年前就被下线了——熔断成功,降级失败,故障从"部分失败"变成"全部失败"。这段代码在正常流量下永远不会被执行。
第二类:人在故障下的行为。 这一类比第一类更重要,也更少被测量。Runbook 里那条 kubectl exec 命令在写的人和评审的人看来都是对的,只有在"另一个人、用另一个账号、在半睡半醒时执行"的时候,才会暴露它跑不通。
2.3 混沌工程成熟的四级(对照自己)
不要一上来就自动化生产环境。按投入产出比排四级:
| 级别 | 做法 | 成本 | 能发现什么 |
|---|---|---|---|
| L1 手动演练 | 预发环境人为制造故障,人跟着 Runbook 走 | 半天人力 | 文档问题、权限问题、判断缺失 |
| L2 工具化实验 | Chaos Mesh 定义故障,命名空间隔离,可重复执行 | 2~4 周建设 | 系统降级路径、告警灵敏度、超时与重试配置 |
| L3 常态实验 | 每周固定时段在预发跑一批实验,结果进看板 | 持续少量维护 | 回归问题:昨天还能扛,今天改了一行就扛不住了 |
| L4 生产实验 | 在业务低峰、小比例真实流量上注入故障 | 需要组织授权 | 只有真实流量才能暴露的东西:真实数据分布、真实依赖、真实负载 |
多数团队的正确起点是 L1,因为它的投入最小,发现的问题却最扎心——上面那四条 Runbook 的三个卡点,L1 全都能抓出来,而 L2 的工具一个都抓不出来。
L4 不必追。它需要的能力(爆炸半径控制、一键终止、业务方参与评估)远高于它带来的增量信息,除非你的系统已经稳定到"预发环境和生产环境行为一致"——而这句话本身通常不成立。
三、爆炸半径:演练与事故的唯一区别
混沌实验和真实事故,技术上做的事情可能完全一样:都是让某个依赖不可用。它们的区别只有一个——爆炸半径是否受控。
所以爆炸半径不是"注意事项",它是这件事能不能做的前提。
3.1 三个维度,一个乘法
爆炸半径 ≈ 故障类型的影响面 × 注入的实例/流量比例 × 持续时间三个维度都要设边界,缺一个就等于没控制:
| 维度 | 可调的边界 | 危险做法 |
|---|---|---|
| 影响面 | 选哪些故障类型(网络延迟 vs 数据删除) | 上 PodKill 但没排除有状态节点 |
| 比例 | 影响 1 个 Pod / 10% 实例 / 5% 流量 | "先注入 100% 试试" |
| 时间 | 单次 ≤ 5 分钟,可随时 terminate | 设 duration: 1h 然后去吃饭 |
三条硬规矩,直接写进实验规范的头部:
- 必须能一键停。 每个实验都要有一个 terminate 命令,写在实验定义旁边,任何人拿到就能执行。
- 必须能自动到期。
duration字段必填,没有"跑完为止"的实验。曾经有过"注入延迟 1 小时,结果实验对象是共享网关,第二天早上才被发现"的事故——注入的东西本身成了故障 - 必须排除关键载体。 有状态组件(数据库、消息队列、etcd)、单点组件(集群唯一的 ingress、唯一的 DNS)、控制面(kube-apiserver、监控本身)。监控被自己的实验打挂,是最讽刺的现场
3.2 用 namespace + label 划物理边界
别靠"我小心点"。边界要用资源对象表达出来。
# 1) 演练专用命名空间,配额锁死上限
apiVersion: v1
kind: Namespace
metadata:
name: chaos-lab
labels:
# 这个 label 是开关:所有 chaos 资源必须落在这里
chaos-mesh.org/inject: enabled
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: lab-quota
namespace: chaos-lab
spec:
hard:
pods: "20"
requests.cpu: "20"
requests.memory: 40GiChaos Mesh 的控制器可以配置只监听带特定 label 的命名空间(--target-namespace-selector),这样即使有人在别的命名空间误建了实验对象,也不会被执行。这不是防恶意,是防手滑。
3.3 生产实验的四道锁
如果一定要上生产(L4),四道锁要同时存在,缺一道就不要做:
锁 1 时间窗:只允许周二/周三 14:00-16:00(业务低峰,且有人在线)
锁 2 比例:最大 5% 流量 或 每可用区 1 个实例,取小
锁 3 熔断:实验前先检查当前错误预算 > 50%,否则实验不启动
锁 4 终止:业务方有独立于 SRE 的停止权限(一句话即可停)第 3 道锁最容易被省,但它最重要:在系统已经不健康的时候做韧性实验,等于在伤口上试刀。 第 19 篇那个 slo-gate.sh 可以直接复用,改一下阈值和放行条件就是一个"实验门禁":
#!/usr/bin/env bash
# chaos-gate.sh —— 生产演练前置检查:预算不足就不许开始
set -euo pipefail
SERVICE="${1:?service required}"
BUDGET=$(curl -sf --get "${PROM_URL}/api/v1/query" \
--data-urlencode "query=slo:period_error_budget_remaining:ratio{slo=\"${SERVICE}-availability\"}" \
| jq -r '.data.result[0].value[1] // "NaN"')
if [ "$BUDGET" = "NaN" ]; then
echo "⛔ 取不到预算,拒绝在无观测的情况下开始演练"; exit 1
fi
PCT=$(awk -v b="$BUDGET" 'BEGIN{printf "%.1f", b*100}')
if awk -v p="$PCT" 'BEGIN{exit !(p <= 50)}'; then
echo "⛔ 当前剩余预算 ${PCT}% ≤ 50%,今天不演练"
echo " 理由:预算不足时,实验造成的额外损耗会直接吃掉用户可用性"
exit 1
fi
echo "✅ 预算 ${PCT}%,可以开始。请确认终止命令已粘贴在手边"最后那句话是有用的。演练开始前,把 terminate 命令贴在终端里,等一切就绪再按回车——这个动作在压力下救人。
四、Chaos Mesh 落地:一个最小可用的生产级配置
工具选型不难。K8s 生态里 Chaos Mesh 是事实标准(第 9 篇的表格、周刊第 1 期都提到过),Litmus 更适合"把实验当 CRD 编进流水线"。下面按 Chaos Mesh 讲,版本以 v2.8 为准。
4.1 选哪些故障类型:先做六个
Chaos Mesh 支持的类型很多(网络、Pod、IO、压力、时间、DNS、HTTP、JVM……),但第一批不要超过六个。按"这个故障在我的故障史里出现过几次"来选:
| 故障类型 | 对应真实故障 | 验证什么 | 优先级 |
|---|---|---|---|
| Network Delay | 下游变慢、跨可用区网络抖动 | 超时设置、熔断阈值、重试次数是否会放大 | 🔴 最高 |
| Network Partition | 网络分区、安全组误配、DNS 部分失败 | 脑裂处理、注册中心剔除、连接池是否泄漏 | 🔴 最高 |
| Pod Kill | 节点驱逐、OOM、发布时滚动重启 | 副本数、PDB、优雅退出、是否有单实例 | 🔴 最高 |
| DNS Error | CoreDNS 挂、解析超时 | 客户端 DNS 缓存、解析失败时的行为 | 🟡 高 |
| Pod Failure(持续不可用,非重启) | 实例假死、探针不通过 | 就绪探针、负载均衡摘除速度 | 🟡 高 |
| Stress(CPU/内存) | 邻居干扰、资源超卖 | 资源限制、QoS、HPA 触发 | 🟢 中 |
没进第一批的:IO 故障(依赖存储的服务少)、Time Skew(时钟偏移,罕见但难查,留到第三批)、HTTP 故障(需要 sidecar,成本高,但做故障注入测试时很有用)、JVM 故障(只对 Java 栈有意义)。
4.2 一份能直接用的实验定义
以"下游 payment-db 变慢"为例——这是最典型也最容易引发雪崩的一类,因为它不会让服务立刻报错,而是让它慢慢排队、连接池耗尽、最后拖垮调用方。
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: order-api-slow-downstream
namespace: chaos-lab
annotations:
# 人为标注:谁发起、为什么、终止命令写在这里
chaos.example.com/owner: team-order
chaos.example.com/ticket: CHAOS-2026-014
chaos.example.com/abort: kubectl -n chaos-lab delete networkchaos order-api-slow-downstream
spec:
action: delay
mode: one # 只挑 1 个 Pod,别用 all
selector:
namespaces: [chaos-lab]
labelSelectors:
app: order-api
delay:
latency: "800ms"
jitter: "100ms"
correlation: "50"
direction: to # order-api 发出去的请求变慢(影响它对下游的调用)
target:
mode: all
selector:
namespaces: [chaos-lab]
labelSelectors:
app: payment-db
duration: "5m" # 强制到期,不是"跑完为止"几个参数值得单独说:
mode: one而不是all。 一个 Pod 变慢,才能看到负载是否被正确转移到其他副本、连接池是否只在那一个实例上耗尽。全量变慢是压测,不是混沌实验jitter必填。 均匀的 800ms 延迟对 TCP 重传和客户端的退避算法来说是"假"的。真实的网络抖动有方差,加上 jitter 才能触发真实的超时行为direction: to+target的组合。 只让 order-api 到 payment-db 的流量变慢,不影响其他地方。如果不指定 target,影响的是这个 Pod 的全部出入流量,噪声会很大,结论也会模糊duration: 5m。 5 分钟足够看到:第 1 分钟超时开始、第 2 分钟重试风暴、第 3 分钟连接池满、第 4 分钟熔断触发、第 5 分钟观察恢复。超过 5 分钟能多看到的信息很少,风险却线性上升
4.3 一次实验的完整流程
① 写假设(先写,不能事后补)
"order-api 对 payment-db 的调用增加 800ms 延迟 5 分钟,
在此期间订单创建成功率不低于 99.5%,
OrderApiHighLatency 告警在 2 分钟内触发,
若熔断触发,应降级到只读模式而不是返回 5xx"
② 记录基线(实验前 15 分钟的正常数据)
SLI / p99 / 错误率 / 连接池使用率 / 关键业务量
③ 注入 → 观察(盯三块屏)
业务指标:下单量、成功率
系统指标:p99、连接池、goroutine/线程数、GC
告警:什么时候响、响了什么、通知到了谁
④ 终止(到点主动停,不要"再等等看")
⑤ 观察恢复(这一步最容易漏)
停掉故障后多久恢复?有没有残留?连接池是否回到基线?
恢复耗时经常比故障期间更能说明问题
⑥ 记录结果:假设成立/不成立,差异在哪第 ⑤ 步单独拎出来讲,是因为它常常比第 ③ 步信息量更大。有一次实验:注入 DNS 故障 3 分钟,故障期间服务正常(客户端有缓存),停掉故障之后 20 分钟才恢复——因为缓存全量过期 + 集中重建,把所有请求压到了下游。这条结论只在"恢复期"才能看到,而它对应的正是第 19 篇讲的"回滚引发二次雪崩"。
4.4 实验即代码:放进仓库,可重复、可回看
不要用 Web UI 点。实验定义进 Git,理由跟基础设施进 Git 一样:可评审、可追溯、可重复。
chaos/
├── experiments/
│ ├── order-api-slow-db.yaml
│ ├── payment-api-kill-pod.yaml
│ ├── gateway-partition-az.yaml
│ └── dns-error-core.yaml
├── hypotheses/ # 假设文档,和实验一一对应
│ └── order-api-slow-db.md
└── README.md # 每个实验的爆炸半径、终止命令、历史结论README.md 里每个实验一行,三列:上次跑是什么时候、结论、现在还能不能跑。第三列常被忽略——半年不跑的实验,对象可能已经改名、架构可能已经变了,直接跑会得到一堆无意义的错误。
五、Game Day:一天的流程
混沌工程是工具,Game Day 是组织。单次实验能验证系统,一整天的 Game Day 才能验证团队。
5.1 提前两周:定范围
游戏日最怕"什么都想练一遍"。一次 Game Day 定三到五个场景,选择标准:
- 故障史里出现过、且大概率还会再出现的(别挑最罕见的)
- 有 Runbook 的(没有 Runbook 的场景先写文档,再来练)
- 能在一小时内结束的(长场景会拖垮精力,下午的效率会明显下降)
场景定完,立刻做一件事:把 Runbook 里涉及的命令,用演练账号跑一遍。 这一步能在 Game Day 之前就抓掉一批"权限不足"类的卡点,避免浪费白天的时间。
5.2 提前一周:写假设,拆角色
每个场景写一段假设(格式同 4.3),然后拆四种角色:
| 角色 | 职责 | 谁来做 |
|---|---|---|
| 参与者(Responder) | 处理故障,只能按 Runbook 动手 | 真 on-call,最好是最近轮值的同学 |
| 注入者(Injector) | 按剧本注入故障,不提示、不帮忙 | SRE,全程闭嘴是最难的部分 |
| 观察者(Observer) | 记录时间线、卡点原话、情绪变化 | 不参与处置的人,最好有第二个人 |
| 业务方(Stakeholder) | 判断影响、模拟对外沟通 | 产品/运营,让他们体验"90 秒说不出影响面"的滋味 |
关于注入者闭嘴:最大的阻力从来不是技术,是"我看到他走错方向了,很想提示他"。一旦提示,你就什么都没测到。 走错方向然后自己发现,是演练里最有价值的一段——它会在复盘里变成一句"Runbook 第 3 步会把人引向错误方向",而这句话永远不会从"被提示之后走对了"里产生。
5.3 当天:一张时间线表
只需要一张表,用来记时间点,不用记内容(内容是复盘的事):
| 时刻 | 事件 | 谁 | 备注 |
| --- | --- | --- | --- |
| 14:00 | 注入开始 | 注入者 | NetworkChaos delay 800ms |
| 14:01 | Responder 收到 page | 告警系统 | 比预期慢(假设是 2 分钟内,实际 1min12s)|
| 14:03 | 开始查面板 | Responder | 看了 4 个面板,第 3 个是错的 |
| 14:07 | 说了一句话:"这跟上次那个不一样" | Responder | ← 关键判断点 |
| 14:09 | 尝试第一个动作,失败 | Responder | 权限不足,Runbook 第 4 步 |
| 14:12 | 找到替代命令 | Responder | Runbook 里没有,靠经验 |
| 14:16 | 恢复 | | 总计 16 分钟 |两列信息的关键度最高:"告警到达的时刻"(衡量 MTTD)和**"第一个正确动作的时刻"**(衡量 MTTK)。这两个差值,比总时长更有改进指向:总时长 16 分钟,但其中 9 分钟花在方向判断上——那就不是"操作慢",是"判断树的入口有问题"。
5.4 当天:不要做的事
- 不要在孩子睡觉时间之后安排。 人累的时候演练,得到的结论是"人累的时候会犯错",这条不用测
- 不要在同一个服务上连着注入两次。 第二次参与者已经有上下文了,测不出真实的 MTTK
- 不要在有人正在处理真实故障时开始。 听起来很蠢,每年都有团队干这种事
- 不要现场修 Runbook。 记录卡点,事后一起改。现场改文档会打断时间线,而且改的往往不是根因
5.5 当天下午:现场复盘,30 分钟
上午演练、下午当场复盘,别拖到一周之后——细节会蒸发。
复盘只问三个问题,按顺序:
- 假设成立吗? 不成立的部分,是系统的问题还是假设的问题(有些假设本身就写错了)
- 哪一步最耗时? 只看时间线里最大的那个间隔
- 如果这一步砍掉,总时长会变成多少? 用这个问题量化改进的价值
不要问"谁的责任"。这个问题一旦出现,后面所有真话都会消失——这是第 4 篇 Blameless Postmortem 的主题,Game Day 的复盘尤其要守住这条线,因为参与者是自愿来的。
六、让演练反哺系统:四个出口
演练最容易死的地方,是"办得挺好,然后就没了"。原因通常是产出只有一份复盘报告。演练的产出必须是可跟踪的条目,落进四个已有的出口。
6.1 出口一:Runbook 的具体修改
这是最直接的出口,也是最容易验证的。每一条卡点,都要变成一个具体的文档改动:
| 演练发现的卡点 | 改法 |
|---|---|
| 命令没权限 | 命令换成演练账号能跑的,或在第 0 段写明"需要申请 xx 角色"+ 申请入口链接 |
| 走错方向 | 第二段"三十秒判断方向"补一条分支;或前移一个更强的判别信号 |
| 依赖某个人 | 把"联系 DBA"改成"先执行 A 探针;DBA 不在线且延迟 > 5s 时执行 B 降级动作" |
| 缺"不该做什么" | 补进"不要做这些"段——这次演练里发现"重启会雪崩",就该写进去 |
| 恢复后没指引 | 补"止血之后"段:残留检查清单 |
改完之后有个便宜的验证动作:换一个人再走一遍同一份 Runbook,看卡点是否消失。 这不叫浪费人力,这叫回归测试。第 18 篇讲的"新人演练"就是这个思路的单人版,Game Day 是它的多人版。
6.2 出口二:告警规则的校准
演练会顺带测出告警的三类问题,而且比任何静态评审都准:
① 该响没响 / 响得太晚。 假设是 2 分钟内触发,实际 6 分钟——去查 for: 时长和窗口,通常是 for: 5m 配了一个 5 分钟的 rate(),加起来 10 分钟才可能响。
② 响起了一堆不相干的。 注入一次延迟,触发了 17 条告警,其中 14 条是同一个根因的下游现象。这是第 6 篇"告警治理"要解决的抑制与聚合问题,演练给了真实的触发顺序。
③ 通知到了错的人。 演练里最贵的一次发现:故障发生在支付,page 却发给了订单团队,因为告警规则是从订单服务复制过来的,team 标签忘了改。这种错误在静态评审里几乎不可能看出来。
对应的动作很具体:把演练中触发的全部告警按"是否指向根因"分成三类(根因、现象、噪声),然后:
- 根因类:检查
for时长,确认它比 Runbook 第 2 段的判断时间短 - 现象类:加抑制规则(
inhibit_rules),挂在根因告警之下 - 噪声类:删掉,或降级为 ticket(第 13 篇的分级词汇)
6.3 出口三:系统本身的修复项
这一类会自然地变成技术债清单,但要区分"必须现在修"和"记下来":
- 必须现在修:任何"设计意图与实际行为不符"的情况。比如上面那个熔断后打到已下线接口的例子——它意味着降级路径整个是坏的,而你以为它是好的
- 必须现在修:单点。演练里杀掉一个 Pod 就造成中断的,说明副本数或 PDB 配置有问题,这类修复通常只改一行 YAML
- 记下来:容量、非关键路径的性能、体验问题
还有一个更狠的判定标准,可以用来排优先级:
如果这次注入发生在生产、发生在业务高峰,会造成多少损失? 用这个数字排优先级,比"感觉重要"可靠得多。
6.4 出口四:把实验变成常态
最后一步,让单次演练变成持续机制:
- 每周固定时段跑一批实验(比如周三 14:00 跑 3 个),结果自动进看板
- 关键实验接进发布流水线:第 19 篇的灰度期间,可以在预发跑一次对应的故障实验,作为"这个版本是否还扛得住"的验证
- 实验结论进服务档案:每个服务的 Wiki 页上有一栏"最近验证过的故障场景与结论",跟 SLO 放在一起
第二点值得展开。灰度和混沌是互补的两件事:灰度验证"新代码在正常流量下会不会引入故障",混沌验证"新代码在故障下还能不能工作"。一次变更之后,如果只跑了灰度没跑实验,那你只验证了一半。
七、度量:怎么知道演练有用
不要用"办了 N 次 Game Day"当指标。用下面五个,前三个度量能力,后两个度量机制。
| 指标 | 定义 | 参考目标 |
|---|---|---|
| Runbook 首次可用率 | 演练中不打求助电话、不靠经验补齐就能走完的 Runbook 占比 | 从 30% 提到 70%+ |
| MTTK(到第一个正确动作) | 从收到告警到执行第一个正确动作的中位时长 | < 5 分钟 |
| 恢复时长 P50 / P90 | 演练场景的恢复耗时,注意看 P90 而不是均值 | 逐季度下降 |
| 实验覆盖率 | 有对应演练场景的核心服务数 / 核心服务总数 | 核心链路 100% |
| 发现项闭环率 | 演练发现的问题在一个季度内修复或显式接受的比例 | > 80% |
第一个指标是这篇文章最想强调的。上面那次演练的首次可用率是 25%(4 条走通 1 条)。这个数字低得吓人,但它不是"团队不行",而是**"没演练过的 Runbook 首次可用率本来就是这个量级"**。
第 18 篇里有个可对照的数字:被新人演练过的 Runbook,实战中被反馈"看不懂/走不通"的比例是 8%,没演练过的是 41%。Game Day 是这个统计的放大版——它测的不只是"看不看得懂",还包括权限、判断、协作。
最后一个指标(闭环率)决定了这件事能不能持续。如果每次演练都产生 12 个发现、只修掉 2 个,第三季度就不会有人愿意办了。 所以宁可少练两个场景,把发现修完。
八、五个真实的坑
坑一:把演练做成了演示
最常见的一种:提前跟参与者对好剧本,演练当天所有动作行云流水,领导看完很满意。这不叫演练,叫彩排。判断标准很简单——如果没人卡住,你就白办了。 那四个 Runbook 的演练之所以有价值,恰恰因为三个人卡住了。
坑二:注入之后不敢停
注入者看到"故障好像真出问题了",第一反应是"再等等,看看会怎么发展"。这个念头是危险的:实验的目的是验证假设,不是观察事故。假设已经被证伪了("系统没扛住"),多等 10 分钟不会得到新结论,只会扩大影响。到 duration 就停,不要"再看看"。
坑三:只练系统,不练人
只做自动化故障注入、只看监控曲线、没有人参与,这就是压测的变体。它能发现系统问题,但发现不了"Runbook 第 3 步会把人引向错误方向"。而后者在真实故障里的代价通常更大。
坑四:一次演练塞进十个场景
上午高密度、下午就萎了。现场观察会发现:第三个场景之后,参与者开始凭肌肉记忆动手,不再看 Runbook——于是你测的不再是"Runbook 好不好",而是"这个人记不记得"。三到五个场景封顶。
坑五:在生产环境做第一次 Game Day
这是个顺序错误。第一次演练一定在预发或演练集群,把流程、角色、记录表、终止机制都跑顺了,再考虑生产小比例。跳过这一步的团队,往往第一次就遇到"注入成功但终止命令没权限"的场面。
九、规则:写下来,贴在墙上
一份一页纸的演练规则,比任何培训都有用。可以直接改成自己团队的版本:
# 故障演练规则 v1(team-order)
1. 每个实验必须有假设,且假设在注入前写下。
2. 每个实验必须有 duration,必须有 terminate 命令,且命令贴在操作者终端里。
3. 爆炸半径三选一:1 个 Pod / ≤10% 实例 / ≤5% 流量。
4. 禁止注入:数据库、消息队列、etcd、kube-apiserver、监控系统、集群唯一的 ingress。
5. 生产实验仅限周二/周三 14:00-16:00,且开始前错误预算 > 50%。
6. 注入者全程不提示。参与者走了弯路也要让他走完。
7. 演练当天必须现场复盘,复盘禁止讨论责任。
8. 每个卡点必须落成一个具体改动(Runbook / 告警 / 代码 / 配置),并进入追踪列表。
9. 一个季度内,每个核心服务至少被演练一次。
10. 演练失败、参与者放弃了,也是一种有效结果,必须如实记录。第 10 条需要单独解释:"演练失败"是最有价值的输出之一。它意味着这个场景在真实世界里会变成一次无法处理的事故。如果为了面子把它记成"基本完成",那这次演练的成本就白花了。
十、Game Day 自检清单(可直接勾选)
准备(提前两周)
- 场景数 3~5 个,全部来自真实故障史
- 每个场景有对应的 Runbook,且用演练账号跑过命令
- 每个场景写了假设,含"系统保持什么"和"人多长时间内做什么"
- 角色已分配:参与者 / 注入者 / 观察者 / 业务方
- 参与者是真实 on-call,不是最熟悉这个服务的人
- 环境隔离完成:独立命名空间 + 资源配额 + Controller 只监听该命名空间
- 每个实验的 terminate 命令已写进实验定义,并打印出来
- 观察者的时间线表格已备好(空表)
当天
- 开始前 15 分钟记了基线数据
- 注入者全程不提示(最难的一条)
- 每 5 分钟记录一次时间线(不写结论,只写事实与原话)
- 到 duration 主动停止,没有"再看看"
- 刻意观察恢复期:多久回基线、有没有残留、有没有二次冲击
- 现场复盘 30 分钟,只问三个问题,不问责任
事后(一周内)
- 每个卡点落成具体改动,写入追踪列表并指定 owner
- 触发的告警按根因/现象/噪声三分,分别处理
- 修复项按"如果发生在业务高峰的损失"排序
- 关键场景编入常态化实验集,定好下次跑的时间
- 更新服务档案里的"已验证故障场景"一栏
十一、从零开始的四阶段路线
阶段一(第 1~2 周):先办一次最土的 Game Day
- 选 3 个最常见故障场景,选 3 个真实 on-call
- 不用任何工具,人工制造故障(改配置、杀进程、拔网络策略)
- 全程录屏 + 一人记时间线
- 当天现场复盘,产出每个场景的 Runbook 改动清单
验收:至少发现 3 个"人/文档/权限"层面的卡点,并全部改掉。
这一步的投入只有半天人力,但它通常能解释掉一半的"为什么恢复那么慢"。
阶段二(第 3~6 周):上 Chaos Mesh,把实验变成代码
- 部署 Chaos Mesh,配置只监听演练命名空间
- 把阶段一的人工故障翻译成 6 个实验定义,进 Git
- 每个实验配一份假设文档
- 建立"实验 → 结论 → 修复项"的追踪表
验收:同一实验可以一键重跑,结论可对比。
阶段三(第 2~3 月):进流水线,进常态
- 关键实验接入发布流水线的预发阶段(配合第 19 篇的灰度)
- 每周固定时段跑一批实验,结果进看板
- 告警规则按演练结果做一轮校准(抑制规则、
for时长、标签) - 核心链路服务覆盖率 100%
验收:连续 4 周固定跑实验,没有一次因为时间不够被跳过。
阶段四(第 4~6 月):小比例生产实验
- 四道锁全部到位(时间窗、比例、预算门禁、业务方终止权)
- 先做最"安全"的故障类型:Pod Kill(K8s 原生就能扛)
- 每次生产实验前,业务方书面确认时间窗
- 建立"实验期间出事怎么办"的预案(谁来停、停了之后谁来看)
验收:生产实验全程无用户可感知影响,且至少得到一个预发环境给不出的结论。
十二、和前面几篇的关系
- 第 18 篇(Runbook 体系):本篇是它的验收手段。第 18 篇给了十四段骨架和"新人演练",本篇把它放大到团队尺度,并且给出了 Runbook 质量的可测量口径——首次可用率
- 第 19 篇(变更安全体系):演练剧本里的"回滚"就是第 19 篇第五节那套;反过来,第 19 篇的灰度验证的是"正常流量下新代码行不行",本篇验证的是"故障下新代码行不行"。两者合起来才是完整的发布验证
- 第 3、10 篇(SLO 与错误预算):预算在这里的用法变了——不是决定"能不能发",而是决定"今天能不能练"。这是一个很有意思的复用
- 第 6、13 篇(告警治理与分级):演练是告警规则最好的回归测试,而且它测出的是静态评审看不见的那一类:通知发给了错的团队
- 第 7、12、14、17 篇(可观测性):演练的"观察"环节完全依赖这四篇的成果。没有按版本、按实例切分的指标,你连"故障是否真的注入了"都判断不了
- 第 8 篇(on-call):本篇测的是 on-call 的真实能力,而第 8 篇讲的是怎么让人能撑住。演练里那句"这跟上次那个不一样",就是经验在起作用的证据
- 第 21 篇(容量规划):本篇的注入比例(5% 流量、1 个实例)需要容量数据来定——你得先知道 1 个实例承载多少、5% 流量是多少 QPS
- 第 23 篇(数据库 SRE):备份恢复演练是混沌工程的一个特例,而且是最应该做的那一个
写在最后
如果这篇只带走一句:
没演练过的预案,等于没有预案。
这句话可以推得更远一点。没演练过的回滚,等于没有回滚;没演练过的降级开关,等于一行装饰性代码;没演练过的 on-call,等于一个需要 40 分钟才能把名字和头像对上的人。它们都存在于文档和配置里,只是从来没在压力下被执行过。
如果只带走一个动作:
从你手上任意一份 Runbook 里,挑第三条命令,找一个不是你写的这份文档的人,用他自己的账号,在预发环境跑一遍。
大概率会卡住。卡住的地方,就是这个季度最值得改的一行。
那个下午,四个人里三个卡住,没有一个人是技术不行。而如果换成某个凌晨三点的真实故障,这些卡点的价格会以分钟计价。演练的全部意义,就是把那些分钟提前花掉,并且花得很便宜。
本文是 SRE 实战系列第 20 篇,也是第二循环的第二篇。第 19 篇让变更不再引入故障,这一篇主动制造故障去证明自己扛得住。下一篇(第 21 篇)换个问题:这套东西都要有分母——你的系统到底能承载多少、还有多少余量、大促之前怎么算。从"省了多少"到"够不够用"。